Dylib Hijacking
0x00 Intro
攻击者可以通过构建指定名称的恶意动态库,将文件存放到受害者应用程序在运行时搜索的路径进行加载。问题的原因有多处,限制也很多,本文也不一定能阐述完全,并且也不会只围绕劫持层面去描述。我了解这个的起点是希望运用Dylib注入来对macOS应用程序的破解进行持久化。
0x01 Dylib Injection ( DYLD_INSERT_LIBRARIES )
https://theevilbit.github.io/posts/dyld_insert_libraries_dylib_injection_in_macos_osx_deep_dive/
1 Desc
DYLD_INSERT_LIBRARIES 注入在macOS中存在很长时间了,通过环境变量的形式对指定的应用程序注入Dylib。引用链接是@Csaba Fitzl在2019年发布的对于这项技术的分析,off sec的macOS Control Bypasses,据ta说也是由ta创建的。
在dyld的man page中描述如下,意思就是可以在程序加载之前加载在此变量中指定的任何dylib。
DYLD_INSERT_LIBRARIES
This is a colon separated list of dynamic libraries to load before the ones specified in the
program. This lets you test new modules of existing dynamic shared libraries that are used in
flat-namespace images by loading a temporary dynamic shared library with just the new modules.
Note that this has no effect on images built a two-level namespace images using a dynamic
shared library unless DYLD_FORCE_FLAT_NAMESPACE is also used.
2 Perform
先创建一个动态库文件
#include <stdio.h>
#include <syslog.h>
__attribute__((constructor))
static void myconstructor(int argc, const char **argv)
{
printf("[+] dylib constructor called from %s\n", argv[0]);
syslog(LOG_ERR, "[+] dylib constructor called from %s\n", argv[0]);
}
然后使用gcc编译成动态库
gcc -dynamiclib example.c -o example.dylib
编写一个hello程序
#import <Foundation/Foundation.h>
#include <stdio.h>
#include <stdlib.h>
int hello() {
printf("Hello from hello function!\n");
return rand();
}
int main() {
printf("Hello World!\n");
int r = hello();
printf("Random number is: %i\n", r);
NSString* hello = @"Hello Obj-C!";
[hello writeToFile:@"hello-objc.txt" atomically:YES encoding:NSASCIIStringEncoding error:nil];
}
使用gcc编译成可执行文件
gcc -framework Foundation hello.m -o hello
然后使用DYLD_INSERT_LIBRARIES注入dylib运行程序
DYLD_INSERT_LIBRARIES=example.dylib ./hello
Soft98: % DYLD_INSERT_LIBRARIES=example.dylib ./hello
[+] dylib constructor called from ./hello
Hello World!
Hello from hello function!
Random number is: 16807
可以看到dylib在加载的时候执行了,但是这只是理想情况下,其实现在大部分应用程序已经无法注入了。
3 Restrictions
网上的资料从代码的维度可以得到无法注入的限制条件,这边列举出来,如果感兴趣可以在引用的链接中查看。
setuid/setgid
包含
__RESTRICT/__restrict
分段签名时设置
hardened runtime/CS_RESTRICT
标志(新版macOS会加上且SIP开启)签名时设置
CS_REQUIRE_LV
标志(校验动态库签名)
当签名标志被设置时,如果entitlement有以下两个也可以被注入
- com.apple.security.cs.allow-dyld-environment-variables
- com.apple.security.cs.disable-library-validation
各限制的检查方法如下:
setuid/setgid
ls -l ./hello
查看是否带有s标志
包含
__RESTRICT/__restrict
分段size -x -l -m ./hello
查看是否有__RESTRICT
的Segment
签名时设置
hardened runtime/CS_RESTRICT
标志(新版macOS会加上且SIP开启)codesign -dv --entitlements :- ./hello
查看是否有flags=0x10000(runtime)/flags=0x800
,是否存在可以被注入的设置csrutil status
查看SIP开启情况- 会有动态签名的情况,codesign查看限制0x0,但是注入不进去
csops -status <pid>
获取Code Signing Status,然后将获取值进行echo $(([##16]<status> & 0x800))
,查看结果是不是800(CS_RESTRICT)
签名时设置
CS_REQUIRE_LV
标志(校验动态库签名)codesign -dv --entitlements :- ./hello
查看是否有flags=0x20000(library-validation)
,是否存在可以被注入的设置
总结来看如果在SIP开启的情况下,只要查看setuid/setgid、__RESTRICT和是否存在特定的entitlement就可以了。
0x02 Dylib Hijacking
Dylib劫持类似于DLL劫持,就是让程序在加载时加载恶意构造的Dylib,加载的Dylib会受上面Dylib注入中提到的限制。程序在加载Dylib时,有4种需要特别关注的加载指令,分别如下:
- LC_LOAD_DYLIB
- 加载Dylib,找不到文件抛出异常
- LC_LOAD_WEAK_DYLIB
- 加载Dylib,找不到文件不会抛出异常
- LC_REEXPORT_DYLIB
- 代理模式加载Dylib,找不到文件抛出异常
- LC_LOAD_UPWARD_DYLIB
- 加载Dylib,存在相互依赖关系,找不到文件抛出异常
这里还有一点需要注意,加载的文件地址中除了绝对路径外,还会有@rpath/xxx/xxx.dylib
的形式,这个`@rpath是在LC_RPATH中自定义的,这个rpath可以定义多个,当加载dylib时会按照rpath的顺序依次搜索是否存在相应的文件,此时就出现了两种劫持的情况。
- LC_LOAD_WEAK_DYLIB 加载的文件不存在
- 创建同名、同版本的dylib放到指定目录
- LC_LOAD_WEAK_DYLIB/LC_LOAD_DYLIB 存在多个rpath目录,第一个目录中文件不存在,第二个中文件存在
- 创建同名、同版本的dylib放到优先级高的目录中
可以使用otool查看程序的LC_COMMAND内容:
otool -l <file_path>
otool -l <file_path> | grep LC_LOAD_WEAK_DYLIB -A 5
otool -l <file_path> | grep LC_RPATH -A 2
otool -l <file_path> | grep @rpath
然后还要对应用程序做上面提到的相关限制的检查,符合注入条件就有劫持的可能,还有就是要注意一个应用程序它所引入的框架也是程序,这些程序也可能引入了一些Dylib,并且可能框架有时会比主程序的限制更宽松。
劫持时使用Dylib在编译时用到的命令如下:
// 编译dylib,适配current_version和compatibility_version,这个具体的值需要自行更改
// -reexport_library 是以代理的方式加载存在的dylib,防止程序崩溃,也就是LC_REEXPORT_DYLIB
gcc -dynamiclib -current_version 1.0 -compatibility_version 1.0 -framework Foundation hijack.m -Wl,-reexport_library,"hijacked_file_path" -o hijack.dylib
// 默认情况代理加载的dylib是以@rpath的形式加载的,修改为绝对路径的形式
install_name_tool -change @rpath/xxxx.dylib "hijacked_file_path" hijack.dylib
然后将文件移到指定的目录,修改成对应的名字就可以了。
还有一种特殊情况是程序中直接使用dlopen方法去加载dylib,可以查看具体的加载路径是否可以被劫持。
因为我最先开始的目的是想要持久化应用程序的相关修改,所以想到这里或许可以以代理模式加载的情况,直接替换掉原有的Dylib。这里说一个开源项目insert_dylib,用于对指定程序注入dylib,它的核心注入方式是在指定程序load命令的末尾增加新的LC_LOAD_DYLIB
然后修复头信息的ncmds和sizeofcmds确保命令被识别,然后如果存在签名还要将签名删掉,--weak
选项是添加LC_LOAD_WEAK_DYLIB
。
0x03 Summary
本文只是简单描述了Dylib劫持的大概内容和相关限制,Dylib注入的方式也并非只有环境变量这一种,这里只是参考其他文件的书写方式将限制条件抛出来。下面就简单列举一下对一个程序该如何判断是否存在Dylib劫持风险。
// 1 查看是否setuid/setgid
ls -l <file_path>
// 2 查看是否包含__RESTRICT/__restrict分段
size -x -l -m <file_path>
// 3 查看程序的flags和entitlement
codesign -dv --entitlements :- <file_path>
// 4 查看是否开启SIP
csrutil status
// === 以上是确保可以加载自定义的Dylib,当然还可以直接使用测试dylib查看是否成功加载
// === DYLD_INSERT_LIBRARIES=example.dylib <file_path>
// 5 确保可以加载自定义Dylib后,对程序的每一个可执行文件查看LC Commands,判断是否存在可利用的文件/目录
otool -l <file_path>
otool -l <file_path> | grep LC_LOAD_WEAK_DYLIB -A 5
otool -l <file_path> | grep LC_RPATH -A 2
otool -l <file_path> | grep @rpath
以下是AI总结:
- 代码签名和强化运行时相关:
- 检查程序是否启用了library validation (代码签名标志CS_REQUIRE_LV)
- 检查是否启用了强化运行时(Hardened Runtime)
- 检查是否有com.apple.security.cs.disable-library-validation权限(允许加载不同团队ID签名的dylib)
- 检查是否有com.apple.security.cs.allow-dyld-environment-variables权限(允许使用DYLD环境变量)
- dylib加载命令相关:
- 检查是否使用了LC_LOAD_WEAK_DYLIB命令加载dylib
- 检查@rpath搜索路径顺序,是否存在可利用的路径
- 检查dlopen()函数调用时是否使用了完整路径
- 检查程序依赖的dylib数量和位置
- 权限和保护相关:
- 检查程序是否受SIP保护
- 检查程序是否有SUID位设置
- 检查程序是否有__RESTRICT段
- 检查目标dylib路径是否可写
- 其他条件:
- 检查程序的Team ID
- 检查dylib的版本兼容性要求
- 检查程序是否导出所需的函数
- 检查是否可以通过插件或扩展方式注入
主要利用方式:
- 使用DYLD_INSERT_LIBRARIES环境变量注入
- dylib劫持/代理攻击
- 通过dlopen函数劫持
- 通过插件/扩展系统注入
判断程序是否可注入的一般规则:
- 如果程序受SIP保护或启用了library validation且没有相关权限,则较难注入
- 如果程序可扩展或存在dylib劫持条件,则可能可以注入
- 需要综合考虑以上所有条件来判断是否存在可利用的注入方式